/*
 * File     : AbstractLibraryBuilder.java
 * Created  : 21 Apr 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.library;

import static com.googlecode.dni.internal.DniInternal.LOGGER;

import java.lang.reflect.Method;
import java.util.List;
import java.util.logging.Level;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;

import com.googlecode.dni.internal.DniInternal;
import com.googlecode.dni.internal.call.JniNativeMethod;
import com.googlecode.dni.internal.call.NativeCallFactory;
import com.googlecode.dni.library.CallingConvention;
import com.googlecode.dni.library.Function;
import com.googlecode.dni.library.Function.LastErrorBehaviour;
import com.googlecode.dni.library.Library;


/**
 * <p>
 *  Generates byte-code for implementations of library types.
 * </p>
 *
 * @author Matthew Wilson
 */
abstract class AbstractLibraryBuilder
{

    private static final String[] SINGLE_EMPTY = { "" };

    private final Class< ? > type;

    private final Library library;

    private final CallingConvention convention;

    private final String charset;

    /** The type implementation name (in byte-code style). */
    private final String implName;

    private NativeCallFactory nativeCallFactory;


    /**
     * @param type
     *            the type (library or structure)
     * @param implName
     *            the implementation name
     * @param library
     *            the library
     * @param convention
     *            the calling convention
     * @param charset
     *            the charset
     */
    AbstractLibraryBuilder( final Class< ? > type,
                            final String implName,
                            final Library library,
                            final CallingConvention convention,
                            final String charset )
    {
        this.type = type;
        this.library = library;
        this.convention = convention;
        this.charset = charset;
        this.implName = implName;
    }

    /**
     * Creates a library instance.
     *
     * @param classWriter
     *            the class writer
     * @param partialLibrary
     *            whether a partial library
     * @param nativeMethods
     *            container to receive created native methods
     */
    final void buildInternal( final ClassWriter classWriter,
                              final boolean partialLibrary,
                              final List< JniNativeMethod > nativeMethods )
    {
        DynamicLibrary dynamicLibrary = getDynamicLibrary();

        if ( dynamicLibrary == null )
        {
            throw new AssertionError();
        }

        this.nativeCallFactory = DniInternal.PLATFORM.createNativeCallFactory( this.convention );
        this.nativeCallFactory.init( this.type, partialLibrary, classWriter, this.implName );

        boolean success = false;
        try
        {
            createMethods( dynamicLibrary, classWriter, nativeMethods );
            success = true;
        }
        finally
        {
            if ( !success )
            {
                try
                {
                    dynamicLibrary.free();
                }
                catch ( Throwable throwable )
                {
                    DniInternal.LOGGER.log( Level.SEVERE, "Failed to free library on other exception", throwable );
                }
            }
        }
    }

    /**
     * @return the type
     */
    final Class< ? > getType()
    {
        return this.type;
    }

    /**
     * @return the library
     */
    final Library getLibrary()
    {
        return this.library;
    }

    /**
     * @return the implName
     */
    final String getImplName()
    {
        return this.implName;
    }

    /**
     * @return the calling convention
     */
    final CallingConvention getConventions()
    {
        return this.convention;
    }

    /**
     * @return the charset
     */
    final String getCharset()
    {
        return this.charset;
    }

    /**
     * Writes out a function method.
     *
     * @param dynamicLibrary
     *            the dynamic library
     * @param method
     *            the Java method on the interface
     * @param nativeMethods
     *            destination list of native methods to register
     */
    final void writeFunctionMethod( final DynamicLibrary dynamicLibrary,
                                    final Method method,
                                    final List< JniNativeMethod > nativeMethods )
    {
        LOGGER.fine( "Creating library method for: " + method );

        String name = this.library.functionPrefix() + method.getName();
        boolean mangle = true;
        String functionCharset = this.charset;
        boolean optional = false;
        LastErrorBehaviour lastErrorBehaviour = LastErrorBehaviour.IGNORE;

        Function function = method.getAnnotation( Function.class );
        if ( function != null )
        {
            name = function.name();
            if ( name.isEmpty() )
            {
                name = this.library.functionPrefix() + method.getName();
            }

            mangle = function.mangleName();

            functionCharset = function.charset();
            if ( functionCharset.isEmpty() || Function.LIBRARY_CHARSET.equals( functionCharset ) )
            {
                functionCharset = this.charset;
            }

            optional = function.optional();
            lastErrorBehaviour = function.lastError();
        }

        assert this.nativeCallFactory != null;
        JniNativeMethod nativeMethod = this.nativeCallFactory.writeMethod( method,
                                                                           dynamicLibrary,
                                                                           name,
                                                                           mangle,
                                                                           functionCharset,
                                                                           optional,
                                                                           lastErrorBehaviour );
        if ( nativeMethod != null )
        {
            nativeMethods.add( nativeMethod );
        }
    }

    /**
     * Writes the static initialiser fragment.
     *
     * @param methodVisitor
     *            the method visitor
     */
    final void writeStaticInitialiserFragmentInternal( final MethodVisitor methodVisitor )
    {
        assert this.nativeCallFactory != null;
        this.nativeCallFactory.writeStaticInitialiserFragment( methodVisitor );
    }

    /**
     * Writes the methods.
     *
     * @param dynamicLibrary
     *            the dynamic library
     * @param classWriter
     *            the class writer
     * @param nativeMethods
     *            container to receive created native methods
     */
    abstract void createMethods( DynamicLibrary dynamicLibrary,
                                 ClassWriter classWriter,
                                 List< JniNativeMethod > nativeMethods );

    /**
     * Finds the dynamic library.
     *
     * @return the dynamic library
     */
    private DynamicLibrary getDynamicLibrary()
    {
        String[] names = this.library.name();
        if ( names.length == 0 )
        {
            names = SINGLE_EMPTY;
        }

        DynamicLibrary dynamicLibrary = null;
        for ( int i = 0; i < names.length; i++ )
        {
            String name = names[ i ];
            try
            {
                dynamicLibrary = DynamicLibrary.open( name, this.library.mangleName() );
                break;
            }
            catch ( LinkageError error )
            {
                if ( i == names.length - 1 )
                {
                    throw error;
                }

                LOGGER.info( "Skipping unloadable library name: " + name );
            }
        }

        return dynamicLibrary;
    }

}
